/*
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 *
 * This software is open source.
 * See the bottom of this file for the licence.
 */

package org.dom4j.io;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Iterator;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.DTD;
import javax.xml.stream.events.EndDocument;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.EntityReference;
import javax.xml.stream.events.ProcessingInstruction;
import javax.xml.stream.events.StartDocument;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.util.XMLEventConsumer;

import org.dom4j.Attribute;
import org.dom4j.Branch;
import org.dom4j.CDATA;
import org.dom4j.Comment;
import org.dom4j.Document;
import org.dom4j.DocumentType;
import org.dom4j.Element;
import org.dom4j.Entity;
import org.dom4j.Namespace;
import org.dom4j.Node;
import org.dom4j.Text;

/**
 * Writes DOM4J {@link Node}s to a StAX event stream. In addition the
 * <code>createXXX</code> methods are provided to directly create STAX events
 * from DOM4J nodes.
 * 
 * @author Christian Niles
 */
public class STAXEventWriter
{
   /** The event stream to which events are written. */
   private XMLEventConsumer consumer;

   /** The event factory used to construct events. */
   private XMLEventFactory factory = XMLEventFactory.newInstance();

   private XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();

   public STAXEventWriter()
   {
   }

   /**
    * Constructs a <code>STAXEventWriter</code> that writes events to the
    * provided file.
    * 
    * @param file The file to which events will be written.
    * 
    * @throws XMLStreamException If an error occurs creating an event writer
    *            from the file.
    * @throws IOException If an error occurs openin the file for writing.
    */
   public STAXEventWriter(File file) throws XMLStreamException, IOException
   {
      consumer = outputFactory.createXMLEventWriter(new FileWriter(file));
   }

   /**
    * Constructs a <code>STAXEventWriter</code> that writes events to the
    * provided character stream.
    * 
    * @param writer The character stream to which events will be written.
    * 
    * @throws XMLStreamException If an error occurs constructing an event writer
    *            from the character stream.
    */
   public STAXEventWriter(Writer writer) throws XMLStreamException
   {
      consumer = outputFactory.createXMLEventWriter(writer);
   }

   /**
    * Constructs a <code>STAXEventWriter</code> that writes events to the
    * provided stream.
    * 
    * @param stream The output stream to which events will be written.
    * 
    * @throws XMLStreamException If an error occurs constructing an event writer
    *            from the stream.
    */
   public STAXEventWriter(OutputStream stream) throws XMLStreamException
   {
      consumer = outputFactory.createXMLEventWriter(stream);
   }

   /**
    * Constructs a <code>STAXEventWriter</code> that writes events to the
    * provided event stream.
    * 
    * @param consumer The event stream to which events will be written.
    */
   public STAXEventWriter(XMLEventConsumer consumer)
   {
      this.consumer = consumer;
   }

   /**
    * Returns a reference to the underlying event consumer to which events are
    * written.
    * 
    * @return The underlying event consumer to which events are written.
    */
   public XMLEventConsumer getConsumer()
   {
      return consumer;
   }

   /**
    * Sets the underlying event consumer to which events are written.
    * 
    * @param consumer The event consumer to which events should be written.
    */
   public void setConsumer(XMLEventConsumer consumer)
   {
      this.consumer = consumer;
   }

   /**
    * Returns a reference to the event factory used to construct STAX events.
    * 
    * @return The event factory used to construct STAX events.
    */
   public XMLEventFactory getEventFactory()
   {
      return factory;
   }

   /**
    * Sets the event factory used to construct STAX events.
    * 
    * @param eventFactory The new event factory.
    */
   public void setEventFactory(XMLEventFactory eventFactory)
   {
      this.factory = eventFactory;
   }

   /**
    * Writes a DOM4J {@link Node}to the stream. This method is simply a gateway
    * to the overloaded methods such as {@link#writeElement(Element)}.
    * 
    * @param n The DOM4J {@link Node}to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeNode(Node n) throws XMLStreamException
   {
      switch(n.getNodeType())
      {
      case Node.ELEMENT_NODE:
         writeElement((Element) n);

         break;

      case Node.TEXT_NODE:
         writeText((Text) n);

         break;

      case Node.ATTRIBUTE_NODE:
         writeAttribute((Attribute) n);

         break;

      case Node.NAMESPACE_NODE:
         writeNamespace((Namespace) n);

         break;

      case Node.COMMENT_NODE:
         writeComment((Comment) n);

         break;

      case Node.CDATA_SECTION_NODE:
         writeCDATA((CDATA) n);

         break;

      case Node.PROCESSING_INSTRUCTION_NODE:
         writeProcessingInstruction((org.dom4j.ProcessingInstruction) n);

         break;

      case Node.ENTITY_REFERENCE_NODE:
         writeEntity((Entity) n);

         break;

      case Node.DOCUMENT_NODE:
         writeDocument((Document) n);

         break;

      case Node.DOCUMENT_TYPE_NODE:
         writeDocumentType((DocumentType) n);

         break;

      default:
         throw new XMLStreamException("Unsupported DOM4J Node: " + n);
      }
   }

   /**
    * Writes each child node within the provided {@link Branch}instance. This
    * method simply iterates through the {@link Branch}'s nodes and calls
    * {@link #writeNode(Node)}.
    * 
    * @param branch The node whose children will be written to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeChildNodes(Branch branch) throws XMLStreamException
   {
      for(int i = 0, s = branch.nodeCount(); i < s; i++)
      {
         Node n = branch.node(i);
         writeNode(n);
      }
   }

   /**
    * Writes a DOM4J {@link Element}node and its children to the stream.
    * 
    * @param elem The {@link Element}node to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeElement(Element elem) throws XMLStreamException
   {
      consumer.add(createStartElement(elem));
      writeChildNodes(elem);
      consumer.add(createEndElement(elem));
   }

   /**
    * Constructs a STAX {@link StartElement}event from a DOM4J {@link Element}.
    * 
    * @param elem The {@link Element}from which to construct the event.
    * 
    * @return The newly constructed {@link StartElement}event.
    */
   public StartElement createStartElement(Element elem)
   {
      // create name
      QName tagName = createQName(elem.getQName());

      // create attribute & namespace iterators
      Iterator attrIter = new AttributeIterator(elem.attributeIterator());
      Iterator nsIter =
         new NamespaceIterator(elem.declaredNamespaces().iterator());

      // create start event
      return factory.createStartElement(tagName, attrIter, nsIter);
   }

   /**
    * Constructs a STAX {@link EndElement}event from a DOM4J {@link Element}.
    * 
    * @param elem The {@link Element}from which to construct the event.
    * 
    * @return The newly constructed {@link EndElement}event.
    */
   public EndElement createEndElement(Element elem)
   {
      QName tagName = createQName(elem.getQName());
      Iterator nsIter =
         new NamespaceIterator(elem.declaredNamespaces().iterator());

      return factory.createEndElement(tagName, nsIter);
   }

   /**
    * Writes a DOM4J {@link Attribute}to the stream.
    * 
    * @param attr The {@link Attribute}to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeAttribute(Attribute attr) throws XMLStreamException
   {
      consumer.add(createAttribute(attr));
   }

   /**
    * Constructs a STAX {@link javax.xml.stream.events.Attribute}event from a
    * DOM4J {@link Attribute}.
    * 
    * @param attr The {@link Attribute}from which to construct the event.
    * 
    * @return The newly constructed {@link javax.xml.stream.events.Attribute}
    *         event.
    */
   public javax.xml.stream.events.Attribute createAttribute(Attribute attr)
   {
      QName attrName = createQName(attr.getQName());
      String value = attr.getValue();

      return factory.createAttribute(attrName, value);
   }

   /**
    * Writes a DOM4J {@link Namespace}to the stream.
    * 
    * @param ns The {@link Namespace}to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeNamespace(Namespace ns) throws XMLStreamException
   {
      consumer.add(createNamespace(ns));
   }

   /**
    * Constructs a STAX {@link javax.xml.stream.events.Namespace}event from a
    * DOM4J {@link Namespace}.
    * 
    * @param ns The {@link Namespace}from which to construct the event.
    * 
    * @return The constructed {@link javax.xml.stream.events.Namespace}event.
    */
   public javax.xml.stream.events.Namespace createNamespace(Namespace ns)
   {
      String prefix = ns.getPrefix();
      String uri = ns.getURI();

      return factory.createNamespace(prefix, uri);
   }

   /**
    * Writes a DOM4J {@link Text}to the stream.
    * 
    * @param text The {@link Text}to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeText(Text text) throws XMLStreamException
   {
      consumer.add(createCharacters(text));
   }

   /**
    * Constructs a STAX {@link Characters}event from a DOM4J {@link Text}.
    * 
    * @param text The {@link Text}from which to construct the event.
    * 
    * @return The constructed {@link Characters}event.
    */
   public Characters createCharacters(Text text)
   {
      return factory.createCharacters(text.getText());
   }

   /**
    * Writes a DOM4J {@link CDATA}to the event stream.
    * 
    * @param cdata The {@link CDATA}to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeCDATA(CDATA cdata) throws XMLStreamException
   {
      consumer.add(createCharacters(cdata));
   }

   /**
    * Constructs a STAX {@link Characters}event from a DOM4J {@link CDATA}.
    * 
    * @param cdata The {@link CDATA}from which to construct the event.
    * 
    * @return The newly constructed {@link Characters}event.
    */
   public Characters createCharacters(CDATA cdata)
   {
      return factory.createCData(cdata.getText());
   }

   /**
    * Writes a DOM4J {@link Comment}to the stream.
    * 
    * @param comment The {@link Comment}to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeComment(Comment comment) throws XMLStreamException
   {
      consumer.add(createComment(comment));
   }

   /**
    * Constructs a STAX {@link javax.xml.stream.events.Comment}event from a
    * DOM4J {@link Comment}.
    * 
    * @param comment The {@link Comment}from which to construct the event.
    * 
    * @return The constructed {@link javax.xml.stream.events.Comment}event.
    */
   public javax.xml.stream.events.Comment createComment(Comment comment)
   {
      return factory.createComment(comment.getText());
   }

   /**
    * Writes a DOM4J {@link ProcessingInstruction}to the stream.
    * 
    * @param pi The {@link ProcessingInstruction}to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeProcessingInstruction(org.dom4j.ProcessingInstruction pi)
      throws XMLStreamException
   {
      consumer.add(createProcessingInstruction(pi));
   }

   /**
    * Constructs a STAX {@link javax.xml.stream.events.ProcessingInstruction}
    * event from a DOM4J {@link ProcessingInstruction}.
    * 
    * @param pi The {@link ProcessingInstruction}from which to construct the
    *           event.
    * 
    * @return The constructed {@link
    *         javax.xml.stream.events.ProcessingInstruction} event.
    */
   public ProcessingInstruction createProcessingInstruction(
      org.dom4j.ProcessingInstruction pi)
   {
      String target = pi.getTarget();
      String data = pi.getText();

      return factory.createProcessingInstruction(target, data);
   }

   /**
    * Writes a DOM4J {@link Entity}to the stream.
    * 
    * @param entity The {@link Entity}to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeEntity(Entity entity) throws XMLStreamException
   {
      consumer.add(createEntityReference(entity));
   }

   /**
    * Constructs a STAX {@link EntityReference}event from a DOM4J {@link
    * Entity}.
    * 
    * @param entity The {@link Entity}from which to construct the event.
    * 
    * @return The constructed {@link EntityReference}event.
    */
   private EntityReference createEntityReference(Entity entity)
   {
      return factory.createEntityReference(entity.getName(), null);
   }

   /**
    * Writes a DOM4J {@link DocumentType}to the stream.
    * 
    * @param docType The {@link DocumentType}to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeDocumentType(DocumentType docType)
      throws XMLStreamException
   {
      consumer.add(createDTD(docType));
   }

   /**
    * Constructs a STAX {@link DTD}event from a DOM4J {@link DocumentType}.
    * 
    * @param docType The {@link DocumentType}from which to construct the event.
    * 
    * @return The constructed {@link DTD}event.
    * 
    * @throws RuntimeException DOCUMENT ME!
    */
   public DTD createDTD(DocumentType docType)
   {
      StringWriter decl = new StringWriter();

      try
      {
         docType.write(decl);
      }
      catch(IOException e)
      {
         throw new RuntimeException("Error writing DTD", e);
      }

      return factory.createDTD(decl.toString());
   }

   /**
    * Writes a DOM4J {@link Document}node, and all its contents, to the stream.
    * 
    * @param doc The {@link Document}to write to the stream.
    * 
    * @throws XMLStreamException If an error occurs writing to the stream.
    */
   public void writeDocument(Document doc) throws XMLStreamException
   {
      consumer.add(createStartDocument(doc));

      writeChildNodes(doc);

      consumer.add(createEndDocument(doc));
   }

   /**
    * Constructs a STAX {@link StartDocument}event from a DOM4J {@link
    * Document}.
    * 
    * @param doc The {@link Document}from which to construct the event.
    * 
    * @return The constructed {@link StartDocument}event.
    */
   public StartDocument createStartDocument(Document doc)
   {
      String encoding = doc.getXMLEncoding();

      if(encoding != null)
      {
         return factory.createStartDocument(encoding);
      }
      else
      {
         return factory.createStartDocument();
      }
   }

   /**
    * Constructs a STAX {@link EndDocument}event from a DOM4J {@link Document}.
    * 
    * @param doc The {@link Document}from which to construct the event.
    * 
    * @return The constructed {@link EndDocument}event.
    */
   public EndDocument createEndDocument(Document doc)
   {
      return factory.createEndDocument();
   }

   /**
    * Constructs a STAX {@link QName}from a DOM4J {@link org.dom4j.QName}.
    * 
    * @param qname The {@link org.dom4j.QName}from which to construct the STAX
    *           {@link QName}.
    * 
    * @return The constructed {@link QName}.
    */
   public QName createQName(org.dom4j.QName qname)
   {
      return new QName(qname.getNamespaceURI(), qname.getName(), qname
         .getNamespacePrefix());
   }

   /**
    * Internal {@link Iterator}implementation used to pass DOM4J {@link
    * Attribute}s to the stream.
    */
   private class AttributeIterator implements Iterator
   {
      /** The underlying DOm4J attribute iterator. */
      private Iterator iter;

      public AttributeIterator(Iterator iter)
      {
         this.iter = iter;
      }

      public boolean hasNext()
      {
         return iter.hasNext();
      }

      public Object next()
      {
         Attribute attr = (Attribute) iter.next();
         QName attrName = createQName(attr.getQName());
         String value = attr.getValue();

         return factory.createAttribute(attrName, value);
      }

      public void remove()
      {
         throw new UnsupportedOperationException();
      }
   }

   /**
    * Internal {@link Iterator}implementation used to pass DOM4J {@link
    * Namespace}s to the stream.
    */
   private class NamespaceIterator implements Iterator
   {
      private Iterator iter;

      public NamespaceIterator(Iterator iter)
      {
         this.iter = iter;
      }

      public boolean hasNext()
      {
         return iter.hasNext();
      }

      public Object next()
      {
         Namespace ns = (Namespace) iter.next();
         String prefix = ns.getPrefix();
         String nsURI = ns.getURI();

         return factory.createNamespace(prefix, nsURI);
      }

      public void remove()
      {
         throw new UnsupportedOperationException();
      }
   }
}

/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided that the
 * following conditions are met:
 * 
 * 1. Redistributions of source code must retain copyright statements and
 * notices. Redistributions must also contain a copy of this document.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * 3. The name "DOM4J" must not be used to endorse or promote products derived
 * from this Software without prior written permission of MetaStuff, Ltd. For
 * written permission, please contact dom4j-info@metastuff.com.
 * 
 * 4. Products derived from this Software may not be called "DOM4J" nor may
 * "DOM4J" appear in their names without prior written permission of MetaStuff,
 * Ltd. DOM4J is a registered trademark of MetaStuff, Ltd.
 * 
 * 5. Due credit should be given to the DOM4J Project - http://www.dom4j.org
 * 
 * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL METASTUFF, LTD. OR ITS CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 */
